﻿
namespace MyCoreLib.Data.Common
{
    using System;
    using System.Collections.Generic;
    using System.Data;
    using System.Diagnostics;
    using System.Linq;

    public abstract class DbClientObject : DisposableBase, IDbClientObject
    {
        private readonly IDbConnection Connection;

        private readonly Stack<DbTransactionObject> Transactions = new Stack<DbTransactionObject>();

        protected DbClientObject(IDbConnection connection)
        {
            if (ReferenceEquals(connection, null))
            {
                throw new ArgumentNullException("connection");
            }
            this.Connection = connection;
        }

        /// <summary>
        /// Gets the underlying connection objects state.
        /// </summary>
        /// <value>
        /// State of the underlying connection object
        /// </value>
        public ConnectionState State 
        {
            get
            {
                return this.Connection.State;
            }
        }

        public IDisposable OpenConnection()
        {
            if (Connection.State == ConnectionState.Open)
            {
                return Disposable.Empty;
            }
            Connection.Open();
            return Disposable.Create(() => Connection.Close());
        }

        

        protected IDbTransaction GetInnerTransaction()
        {
            return Transactions.Select(to=>to.Transaction).FirstOrDefault();
        }

        IDbTransactionObject IDbClientObject.BeginTransaction()
        {
            return this.BeginTransaction();
        }

        IDbTransactionObject IDbClientObject.BeginTransaction(IsolationLevel isolationLevel)
        {
            return this.BeginTransaction(isolationLevel);
        }

        protected abstract IDbClientObject CreateCopy();

        protected abstract DbTransactionObject CreateTransactionObject(IDbTransaction dbTransaction);


        [DebuggerStepThrough]
        public DbTransactionObject BeginTransaction()
        {
            return this.BeginTransactionInternal(null);
        }

        [DebuggerStepThrough]
        public DbTransactionObject BeginTransaction(IsolationLevel isolationLevel)
        {
            return this.BeginTransactionInternal(isolationLevel);
        }

        private DbTransactionObject BeginTransactionInternal(IsolationLevel? isolationLevel)
        {
            bool autoManageConnection =
                    this.Connection.State == ConnectionState.Closed;
            try
            {                
                if (autoManageConnection)
                {
                    this.Connection.Open();
                }
                var dbTransaction = isolationLevel.HasValue
                    ? this.Connection.BeginTransaction(isolationLevel.Value)
                    : this.Connection.BeginTransaction();
                var transactionObject = this.CreateTransactionObject(dbTransaction);
                transactionObject.Commiting += () => this.CommitAllInnerTransactions(transactionObject);
                transactionObject.Committed += () => this.PopTransactionObject(transactionObject);
                if (autoManageConnection)
                {
                    transactionObject.Disposed += this.CloseConnection;
                    transactionObject.Committed += this.CloseConnection;
                    transactionObject.RolledBack += this.CloseConnection;
                }
                this.Transactions.Push(transactionObject);
                return transactionObject;
            }
            catch (Exception)
            {
                this.Connection.Close();
                this.Transactions.Clear();
                throw;
            }
        }




        private void CommitAllInnerTransactions(DbTransactionObject transactionObject)
        {
            while (Transactions.Contains(transactionObject)
                    && Transactions.Peek() != transactionObject)
            {
                var innerTransaction = this.Transactions.Pop();
                innerTransaction.Commit();
            }
        }

        private void PopTransactionObject(DbTransactionObject transactionObject)
        {
            if (Transactions.Peek() != transactionObject)
            {
                throw new InvalidOperationException();
            }
            Transactions.Pop();
        }

        private void CloseConnection()
        {
            if (Connection.State == ConnectionState.Closed)
            {
                return;
            }
            while (Transactions.Count > 0)
            {
                using (Transactions.Pop())
                {
                }
            }
            Connection.Close();
        }

        object ICloneable.Clone()
        {
            return this.CreateCopy();
        }
    }
}
